/**
 * 系统框架核心:反射调用工具，提供以下功能
 * 1、调用接口有效性验证；
 * 2、TODO CC防御；
 * 3、接口访问权限验证；
 * 4、接口调度；
 * 
 * @author create lms 2022.07.21
 */
package ms.core.gateway.tool;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import ms.core.common.ApiResult;
import ms.core.common.AppException;
import ms.core.common.MediaType;
import ms.core.common.MediaType.ContentType;
import ms.core.common.consts.SysErr;
import ms.core.common.utils.RequestUtils;
import ms.core.gateway.dto.Api;
import ms.core.gateway.dto.LWGAuth;
import ms.core.gateway.system.Application;
import ms.core.gateway.tool.SessionTool.AuthSession;
import ms.core.gateway.tool.SessionTool.Session;
import ms.core.gateway.utils.LWGUtils;
import ms.core.gateway.utils.LWGUtils.LWGClient;
import ms.core.gateway.utils.LWGUtils.LWGClient.Server;
import ms.core.gateway.utils.SysVisitUtils;
import ms.core.tool.DateTimeTool;
import ms.core.tool.JsonTool;
import ms.core.tool.StrTool;
import ms.core.tool.SysTool;

public class ReflectTool {

	/**
	 * 通过api授权验证接口访问权限
	 * 
	 * @param api
	 * @param req
	 * @param resp
	 */
	private static void checkApiAccessByAuthorization(Api api, HttpServletRequest req, HttpServletResponse resp) {
		String clientId = req.getParameter("clientId");
		String token = req.getParameter("token");

		// 1.检查登录状态
		AuthSession ssn = SessionTool.getAuthSession(clientId);
		if (ssn==null || !token.equalsIgnoreCase(ssn.getToken())) 
			throw new AppException(SysErr.SYS_ACCESS_FORBID, "无效客户端Id或token已过期.");

		// 2.检查接口是否登录即可访问
		if (Api.LOGIN.equalsIgnoreCase(api.getAcl())) return;

		// 3.检查接口访问权限
		if (ssn.getFuncs()==null || ssn.getFuncs().size()==0)
			throw new AppException(SysErr.USER_NO_RIGHT);

		// 4.判断用户的权限是否包含所要访问api权限，如果没有，则检查 依赖权限是否包含所访问接口权限
		if (ssn.getFuncs().indexOf(api.getApiName())<0)
			throw new AppException(SysErr.USER_NO_RIGHT);	
	}

	/**
	 * 通过用户会话验证接口访问权限
	 * 
	 * @param api
	 * @param req
	 * @param resp
	 */
	private static void checkApiAccessBySession(Api api, HttpServletRequest req, HttpServletResponse resp) {
		String sid = CookieTool.getSid(req);

		// 1.检查登录状态
		Session ssn = SessionTool.getSession(sid);
		if (ssn==null || !CookieTool.logined(req)) {
			CookieTool.invalidCookie(resp);
			throw new AppException(SysErr.USER_NOT_LOGIN);
		}

		// 2.检查接口是否登录即可访问
		if (Api.LOGIN.equalsIgnoreCase(api.getAcl())) return;

		// 3.检查接口访问权限
		if (ssn.getFuncs()==null || ssn.getFuncs().size()==0)
			throw new AppException(SysErr.USER_NO_RIGHT);

		// 4.判断用户的权限是否包含所要访问api权限，如果没有，则检查 依赖权限是否包含所访问接口权限
		if (ssn.getFuncs().indexOf(api.getApiName())<0)
			throw new AppException(SysErr.USER_NO_RIGHT);	
	}

	/**
	 * api访问鉴权
	 * 1、检查接口是否需要登录访问,不要登录则放行
	 * 2、如果需要登录，则检查会话用户是否已登录，未登录状态则不允许访问
	 * 
	 * @param api 访问的api对象
	 * @param req
	 * @param resp
	 * @return
	 */
	private static void checkApiAccess(Api api, HttpServletRequest req, HttpServletResponse resp) {
		// 是否需要登录访问
		if (StrTool.isBlank(api.getAcl()) || Api.NONE.equalsIgnoreCase(api.getAcl())) return;

		// 访问鉴权
		if (api.getType()==Api.PUB_API)
			checkApiAccessByAuthorization(api, req, resp);
		else
			checkApiAccessBySession(api, req, resp);
	}

	/**
	 * 获取请求体
	 * 
	 * @param api
	 * @param request
	 * @return
	 */
	private static Map<String, Object> getRequestBody(Api api, HttpServletRequest request){
		String method = request.getMethod().toLowerCase();
		if (method.equals("get")) return null;

		// 获取http头
		MediaType mediaType = RequestUtils.getMediaType(request);
		if (mediaType==null) 
			throw new AppException(SysErr.SYS_CONTENT_TYPE_ERR, "缺少http协议头信息!");

		ContentType ct = mediaType.contentType();
		if (ct!=ContentType.appJson && ct!=ContentType.appText && ct!=ContentType.txtPlain && ct!=ContentType.formEncoded && ct!=ContentType.formMultiPart)
			throw new AppException(SysErr.SYS_CONTENT_TYPE_ERR, "不支持的http协议头信息!");

		Map<String, Object> body = null;
		if (ct==ContentType.formEncoded) body = RequestUtils.getFormBody(request);
		else if (ct==ContentType.formMultiPart) {
			if (StrTool.isBlank(Application.DIR_FILE_SAVETO)) 
				throw new AppException(SysErr.FILE_ACCESS_ERR, "系统未配置附件存储目录!");

			body = RequestUtils.getMultipartBody(request, Application.DIR_FILE_CACHE, Application.DIR_FILE_SAVETO, api.getFileFilter(), api.getFileSize(), api.getFileNum());
		} else {
			body = RequestUtils.getJsonBody(request);
		}
		
		return body;
	}

	/**
	 * 调用网关实现的类接口
	 * 
	 * @param api
	 * @param body
	 * @param request
	 * @param response
	 * @return
	 * @throws Exception 
	 */
	private static ApiResult localCall(Api api, Map<String, Object> body, HttpServletRequest request, HttpServletResponse response) throws Exception {
		String sid = CookieTool.getSid(request);
		Class<?> cls=Class.forName(api.getClassName());
		Method method = cls.getMethod(api.getFunction(), HttpServletRequest.class, HttpServletResponse.class, Map.class, String.class);
		Object ret = method.invoke(cls.newInstance(), request, response, body, sid);
		return (ret==null) ? null: (ApiResult) ret;
	}
	
	/**
	 * 调用微服务实现的接口
	 * 
	 * @param api
	 * @param body
	 * @param request
	 * @param response
	 * @return
	 */
	private static ApiResult remoteCall(Api api, Map<String, Object> body, HttpServletRequest request, HttpServletResponse response) {
		LWGClient client = LWGUtils.getLWGClient(api.getClientId());
		if (client==null)
			throw new AppException(SysErr.SYS_CLIENT_NOT_FOUND);

		if (client.getServs()==null || client.getServs().size()<1)
			throw new AppException(SysErr.SYS_CLIENT_NOT_STARTED);

		String sid = CookieTool.getSid(request);
		Session ssn = SessionTool.getSession(sid);
		LWGAuth auth = new LWGAuth();
		auth.setClientId(client.getClientId());
		auth.setT(DateTimeTool.dateToUnixTime(new Date()));
		auth.setSignature(client.getSignature(auth.getT()));
		auth.setUserUid(ssn!=null ? ssn.getUserUid() : null);
		auth.setAccount(ssn!=null ? ssn.getAccount() : null);
		auth.setCorpUid(ssn!=null ? ssn.getCorpUid() : null);
		auth.setCtype(ssn!=null ? ssn.getCtype() : -1);
		auth.setRoot(ssn!=null ? ssn.getRoot() : -1);

		// 随机取一个服务(同个类型微服务，部署多个情况下)
		int idx = SysTool.getRandomNum(0, client.getServs().size()-1);
		Server svr = client.getServs().get(idx);
		return FeignTool.callFunc(svr.getUrl(), api.getFunction(), auth, body);
	}
	
	/**
	 * 获取用于记录日志参数
	 * @param body
	 * @return
	 */
	private static String getRequestParamForLog(Map<String, Object> body) {
		Map<String, Object> ret = new HashMap<>();
		for (Map.Entry<String, Object> e: body.entrySet()) {
			Object v = e.getValue();
			if (v instanceof String) {
				String tmp = e.getValue().toString();
				if (tmp.length()>=1024) v = StrTool.leftSubString(tmp, 0, 512)+"...";
			}
			ret.put(e.getKey(), v);
		}
		
		return JsonTool.objectToJson(ret);
	}

	/**
	 * 接口调用主入口，根据客户端调用api接口名称，取出相应类并反射调用
	 * 
	 * @param api 功能接口对象
	 * @param sid 会话id
	 * @param request HttpServletRequest
	 * @param response HttpServletResponse
	 * @return ApiResult
	 * @throws Exception 
	 */
	public static ApiResult call(Api api, HttpServletRequest request, HttpServletResponse response) throws Exception{
		// 1.验证api合法性
		if (api==null || StrTool.isBlank(api.getFunction()))
			throw new AppException(SysErr.SYS_UNKNOWN_FUNC);

		if (StrTool.isBlank(api.getClassName()) && StrTool.isBlank(api.getClientId()))
			throw new AppException(SysErr.SYS_REALIZATION_ERR);

		// 2.访问鉴权(内部api和公开api)
		checkApiAccess(api, request, response);

		// 3.调用
		ApiResult ret = null;
		Map<String, Object> body = getRequestBody(api, request);
		if (StrTool.isBlank(api.getClassName()))
			ret = remoteCall(api, body, request, response);
		else
			ret = localCall(api, body, request, response);

		String ip = RequestUtils.getRequestRemoteIp(request);
		// 4.写入访问日志
		if (Application.LOGEVENT && api.isLogEvent()) {
			String module = StrTool.isBlank(api.getClassName()) ? api.getClientId() : api.getClassName();
			String param = api.isLogParam() ? getRequestParamForLog(body) : null;
			
			String sid = CookieTool.getSid(request);
			Session ssn = SessionTool.getSession(sid);
			String account = ssn==null ? null : ssn.getAccount();
			String userUid = ssn==null ? null : ssn.getUserUid();
			String corpUid = ssn==null ? null : ssn.getCorpUid();
			String userNick= ssn==null ? null : ssn.getNick();
			SysVisitUtils.addVisit(module, api.getApiName(), api.getDescription(), param, account, userNick, userUid, corpUid, ip);
		}

		return ret;
	}
}
